/*! Select for DataTables 1.3.1
 * 2015-2019 SpryMedia Ltd - datatables.net/license/mit
 */

/**
 * @summary     Select for DataTables
 * @description A collection of API methods, events and buttons for DataTables
 *   that provides selection options of the items in a DataTable
 * @version     1.3.1
 * @file        dataTables.select.js
 * @author      SpryMedia Ltd (www.sprymedia.co.uk)
 * @contact     datatables.net/forums
 * @copyright   Copyright 2015-2019 SpryMedia Ltd.
 *
 * This source file is free software, available under the following license:
 *   MIT license - http://datatables.net/license/mit
 *
 * This source file is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE. See the license files for details.
 *
 * For details please refer to: http://www.datatables.net/extensions/select
 */
(function (factory) {
    if (typeof define === 'function' && define.amd) {
        // AMD
        define(['jquery', 'datatables.net'], function ($) {
            return factory($, window, document);
        });
    } else if (typeof exports === 'object') {
        // CommonJS
        module.exports = function (root, $) {
            if (!root) {
                root = window;
            }

            if (!$ || !$.fn.dataTable) {
                $ = require('datatables.net')(root, $).$;
            }

            return factory($, root, root.document);
        };
    } else {
        // Browser
        factory(jQuery, window, document);
    }
}(function ($, window, document, undefined) {
    'use strict';
    var DataTable = $.fn.dataTable;


// Version information for debugger
    DataTable.select = {};

    DataTable.select.version = '1.3.1';

    DataTable.select.init = function (dt) {
        var ctx = dt.settings()[0];
        var init = ctx.oInit.select;
        var defaults = DataTable.defaults.select;
        var opts = init === undefined ?
            defaults :
            init;

        // Set defaults
        var items = 'row';
        var style = 'api';
        var blurable = false;
        var toggleable = true;
        var info = true;
        var selector = 'td, th';
        var className = 'selected';
        var setStyle = false;

        ctx._select = {};

        // Initialisation customisations
        if (opts === true) {
            style = 'os';
            setStyle = true;
        } else if (typeof opts === 'string') {
            style = opts;
            setStyle = true;
        } else if ($.isPlainObject(opts)) {
            if (opts.blurable !== undefined) {
                blurable = opts.blurable;
            }

            if (opts.toggleable !== undefined) {
                toggleable = opts.toggleable;
            }

            if (opts.info !== undefined) {
                info = opts.info;
            }

            if (opts.items !== undefined) {
                items = opts.items;
            }

            if (opts.style !== undefined) {
                style = opts.style;
                setStyle = true;
            } else {
                style = 'os';
                setStyle = true;
            }

            if (opts.selector !== undefined) {
                selector = opts.selector;
            }

            if (opts.className !== undefined) {
                className = opts.className;
            }
        }

        dt.select.selector(selector);
        dt.select.items(items);
        dt.select.style(style);
        dt.select.blurable(blurable);
        dt.select.toggleable(toggleable);
        dt.select.info(info);
        ctx._select.className = className;


        // Sort table based on selected rows. Requires Select Datatables extension
        $.fn.dataTable.ext.order['select-checkbox'] = function (settings, col) {
            return this.api().column(col, {order: 'index'}).nodes().map(function (td) {
                if (settings._select.items === 'row') {
                    return $(td).parent().hasClass(settings._select.className);
                } else if (settings._select.items === 'cell') {
                    return $(td).hasClass(settings._select.className);
                }
                return false;
            });
        };

        // If the init options haven't enabled select, but there is a selectable
        // class name, then enable
        if (!setStyle && $(dt.table().node()).hasClass('selectable')) {
            dt.select.style('os');
        }
    };

    /*

    Select is a collection of API methods, event handlers, event emitters and
    buttons (for the `Buttons` extension) for DataTables. It provides the following
    features, with an overview of how they are implemented:

    ## Selection of rows, columns and cells. Whether an item is selected or not is
       stored in:

    * rows: a `_select_selected` property which contains a boolean value of the
      DataTables' `aoData` object for each row
    * columns: a `_select_selected` property which contains a boolean value of the
      DataTables' `aoColumns` object for each column
    * cells: a `_selected_cells` property which contains an array of boolean values
      of the `aoData` object for each row. The array is the same length as the
      columns array, with each element of it representing a cell.

    This method of using boolean flags allows Select to operate when nodes have not
    been created for rows / cells (DataTables' defer rendering feature).

    ## API methods

    A range of API methods are available for triggering selection and de-selection
    of rows. Methods are also available to configure the selection events that can
    be triggered by an end user (such as which items are to be selected). To a large
    extent, these of API methods *is* Select. It is basically a collection of helper
    functions that can be used to select items in a DataTable.

    Configuration of select is held in the object `_select` which is attached to the
    DataTables settings object on initialisation. Select being available on a table
    is not optional when Select is loaded, but its default is for selection only to
    be available via the API - so the end user wouldn't be able to select rows
    without additional configuration.

    The `_select` object contains the following properties:

    ```
    {
        items:string       - Can be `rows`, `columns` or `cells`. Defines what item
                             will be selected if the user is allowed to activate row
                             selection using the mouse.
        style:string       - Can be `none`, `single`, `multi` or `os`. Defines the
                             interaction style when selecting items
        blurable:boolean   - If row selection can be cleared by clicking outside of
                             the table
        toggleable:boolean - If row selection can be cancelled by repeated clicking
                             on the row
        info:boolean       - If the selection summary should be shown in the table
                             information elements
    }
    ```

    In addition to the API methods, Select also extends the DataTables selector
    options for rows, columns and cells adding a `selected` option to the selector
    options object, allowing the developer to select only selected items or
    unselected items.

    ## Mouse selection of items

    Clicking on items can be used to select items. This is done by a simple event
    handler that will select the items using the API methods.

     */


    /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
     * Local functions
     */

    /**
     * Add one or more cells to the selection when shift clicking in OS selection
     * style cell selection.
     *
     * Cell range is more complicated than row and column as we want to select
     * in the visible grid rather than by index in sequence. For example, if you
     * click first in cell 1-1 and then shift click in 2-2 - cells 1-2 and 2-1
     * should also be selected (and not 1-3, 1-4. etc)
     *
     * @param  {DataTable.Api} dt   DataTable
     * @param  {object}        idx  Cell index to select to
     * @param  {object}        last Cell index to select from
     * @private
     */
    function cellRange(dt, idx, last) {
        var indexes;
        var columnIndexes;
        var rowIndexes;
        var selectColumns = function (start, end) {
            if (start > end) {
                var tmp = end;
                end = start;
                start = tmp;
            }

            var record = false;
            return dt.columns(':visible').indexes().filter(function (i) {
                if (i === start) {
                    record = true;
                }

                if (i === end) { // not else if, as start might === end
                    record = false;
                    return true;
                }

                return record;
            });
        };

        var selectRows = function (start, end) {
            var indexes = dt.rows({search: 'applied'}).indexes();

            // Which comes first - might need to swap
            if (indexes.indexOf(start) > indexes.indexOf(end)) {
                var tmp = end;
                end = start;
                start = tmp;
            }

            var record = false;
            return indexes.filter(function (i) {
                if (i === start) {
                    record = true;
                }

                if (i === end) {
                    record = false;
                    return true;
                }

                return record;
            });
        };

        if (!dt.cells({selected: true}).any() && !last) {
            // select from the top left cell to this one
            columnIndexes = selectColumns(0, idx.column);
            rowIndexes = selectRows(0, idx.row);
        } else {
            // Get column indexes between old and new
            columnIndexes = selectColumns(last.column, idx.column);
            rowIndexes = selectRows(last.row, idx.row);
        }

        indexes = dt.cells(rowIndexes, columnIndexes).flatten();

        if (!dt.cells(idx, {selected: true}).any()) {
            // Select range
            dt.cells(indexes).select();
        } else {
            // Deselect range
            dt.cells(indexes).deselect();
        }
    }

    /**
     * Disable mouse selection by removing the selectors
     *
     * @param {DataTable.Api} dt DataTable to remove events from
     * @private
     */
    function disableMouseSelection(dt) {
        var ctx = dt.settings()[0];
        var selector = ctx._select.selector;

        $(dt.table().container())
            .off('mousedown.dtSelect', selector)
            .off('mouseup.dtSelect', selector)
            .off('click.dtSelect', selector);

        $('body').off('click.dtSelect' + _safeId(dt.table().node()));
    }

    /**
     * Attach mouse listeners to the table to allow mouse selection of items
     *
     * @param {DataTable.Api} dt DataTable to remove events from
     * @private
     */
    function enableMouseSelection(dt) {
        var container = $(dt.table().container());
        var ctx = dt.settings()[0];
        var selector = ctx._select.selector;
        var matchSelection;

        container
            .on('mousedown.dtSelect', selector, function (e) {
                // Disallow text selection for shift clicking on the table so multi
                // element selection doesn't look terrible!
                if (e.shiftKey || e.metaKey || e.ctrlKey) {
                    container
                        .css('-moz-user-select', 'none')
                        .one('selectstart.dtSelect', selector, function () {
                            return false;
                        });
                }

                if (window.getSelection) {
                    matchSelection = window.getSelection();
                }
            })
            .on('mouseup.dtSelect', selector, function () {
                // Allow text selection to occur again, Mozilla style (tested in FF
                // 35.0.1 - still required)
                container.css('-moz-user-select', '');
            })
            .on('click.dtSelect', selector, function (e) {
                var items = dt.select.items();
                var idx;

                // If text was selected (click and drag), then we shouldn't change
                // the row's selected state
                if (matchSelection) {
                    var selection = window.getSelection();

                    // If the element that contains the selection is not in the table, we can ignore it
                    // This can happen if the developer selects text from the click event
                    if (!selection.anchorNode || $(selection.anchorNode).closest('table')[0] === dt.table().node()) {
                        if (selection !== matchSelection) {
                            return;
                        }
                    }
                }

                var ctx = dt.settings()[0];
                var wrapperClass = $.trim(dt.settings()[0].oClasses.sWrapper).replace(/ +/g, '.');

                // Ignore clicks inside a sub-table
                if ($(e.target).closest('div.' + wrapperClass)[0] != dt.table().container()) {
                    return;
                }

                var cell = dt.cell($(e.target).closest('td, th'));

                // Check the cell actually belongs to the host DataTable (so child
                // rows, etc, are ignored)
                if (!cell.any()) {
                    return;
                }

                var event = $.Event('user-select.dt');
                eventTrigger(dt, event, [items, cell, e]);

                if (event.isDefaultPrevented()) {
                    return;
                }

                var cellIndex = cell.index();
                if (items === 'row') {
                    idx = cellIndex.row;
                    typeSelect(e, dt, ctx, 'row', idx);
                } else if (items === 'column') {
                    idx = cell.index().column;
                    typeSelect(e, dt, ctx, 'column', idx);
                } else if (items === 'cell') {
                    idx = cell.index();
                    typeSelect(e, dt, ctx, 'cell', idx);
                }

                ctx._select_lastCell = cellIndex;
            });

        // Blurable
        $('body').on('click.dtSelect' + _safeId(dt.table().node()), function (e) {
            if (ctx._select.blurable) {
                // If the click was inside the DataTables container, don't blur
                if ($(e.target).parents().filter(dt.table().container()).length) {
                    return;
                }

                // Ignore elements which have been removed from the DOM (i.e. paging
                // buttons)
                if ($(e.target).parents('html').length === 0) {
                    return;
                }

                // Don't blur in Editor form
                if ($(e.target).parents('div.DTE').length) {
                    return;
                }

                clear(ctx, true);
            }
        });
    }

    /**
     * Trigger an event on a DataTable
     *
     * @param {DataTable.Api} api      DataTable to trigger events on
     * @param  {boolean}      selected true if selected, false if deselected
     * @param  {string}       type     Item type acting on
     * @param  {boolean}      any      Require that there are values before
     *     triggering
     * @private
     */
    function eventTrigger(api, type, args, any) {
        if (any && !api.flatten().length) {
            return;
        }

        if (typeof type === 'string') {
            type = type + '.dt';
        }

        args.unshift(api);

        $(api.table().node()).trigger(type, args);
    }

    /**
     * Update the information element of the DataTable showing information about the
     * items selected. This is done by adding tags to the existing text
     *
     * @param {DataTable.Api} api DataTable to update
     * @private
     */
    function info(api) {
        var ctx = api.settings()[0];

        if (!ctx._select.info || !ctx.aanFeatures.i) {
            return;
        }

        if (api.select.style() === 'api') {
            return;
        }

        var rows = api.rows({selected: true}).flatten().length;
        var columns = api.columns({selected: true}).flatten().length;
        var cells = api.cells({selected: true}).flatten().length;

        var add = function (el, name, num) {
            el.append($('<span class="select-item"/>').append(api.i18n(
                'select.' + name + 's',
                {_: '%d ' + name + 's selected', 0: '', 1: '1 ' + name + ' selected'},
                num
            )));
        };

        // Internal knowledge of DataTables to loop over all information elements
        $.each(ctx.aanFeatures.i, function (i, el) {
            el = $(el);

            var output = $('<span class="select-info"/>');
            add(output, 'row', rows);
            add(output, 'column', columns);
            add(output, 'cell', cells);

            var exisiting = el.children('span.select-info');
            if (exisiting.length) {
                exisiting.remove();
            }

            if (output.text() !== '') {
                el.append(output);
            }
        });
    }

    /**
     * Initialisation of a new table. Attach event handlers and callbacks to allow
     * Select to operate correctly.
     *
     * This will occur _after_ the initial DataTables initialisation, although
     * before Ajax data is rendered, if there is ajax data
     *
     * @param  {DataTable.settings} ctx Settings object to operate on
     * @private
     */
    function init(ctx) {
        var api = new DataTable.Api(ctx);

        // Row callback so that classes can be added to rows and cells if the item
        // was selected before the element was created. This will happen with the
        // `deferRender` option enabled.
        //
        // This method of attaching to `aoRowCreatedCallback` is a hack until
        // DataTables has proper events for row manipulation If you are reviewing
        // this code to create your own plug-ins, please do not do this!
        ctx.aoRowCreatedCallback.push({
            fn: function (row, data, index) {
                var i, ien;
                var d = ctx.aoData[index];

                // Row
                if (d._select_selected) {
                    $(row).addClass(ctx._select.className);
                }

                // Cells and columns - if separated out, we would need to do two
                // loops, so it makes sense to combine them into a single one
                for (i = 0, ien = ctx.aoColumns.length; i < ien; i++) {
                    if (ctx.aoColumns[i]._select_selected || (d._selected_cells && d._selected_cells[i])) {
                        $(d.anCells[i]).addClass(ctx._select.className);
                    }
                }
            },
            sName: 'select-deferRender'
        });

        // On Ajax reload we want to reselect all rows which are currently selected,
        // if there is an rowId (i.e. a unique value to identify each row with)
        api.on('preXhr.dt.dtSelect', function () {
            // note that column selection doesn't need to be cached and then
            // reselected, as they are already selected
            var rows = api.rows({selected: true}).ids(true).filter(function (d) {
                return d !== undefined;
            });

            var cells = api.cells({selected: true}).eq(0).map(function (cellIdx) {
                var id = api.row(cellIdx.row).id(true);
                return id ?
                    {row: id, column: cellIdx.column} :
                    undefined;
            }).filter(function (d) {
                return d !== undefined;
            });

            // On the next draw, reselect the currently selected items
            api.one('draw.dt.dtSelect', function () {
                api.rows(rows).select();

                // `cells` is not a cell index selector, so it needs a loop
                if (cells.any()) {
                    cells.each(function (id) {
                        api.cells(id.row, id.column).select();
                    });
                }
            });
        });

        // Update the table information element with selected item summary
        api.on('draw.dtSelect.dt select.dtSelect.dt deselect.dtSelect.dt info.dt', function () {
            info(api);
        });

        // Clean up and release
        api.on('destroy.dtSelect', function () {
            disableMouseSelection(api);
            api.off('.dtSelect');
        });
    }

    /**
     * Add one or more items (rows or columns) to the selection when shift clicking
     * in OS selection style
     *
     * @param  {DataTable.Api} dt   DataTable
     * @param  {string}        type Row or column range selector
     * @param  {object}        idx  Item index to select to
     * @param  {object}        last Item index to select from
     * @private
     */
    function rowColumnRange(dt, type, idx, last) {
        // Add a range of rows from the last selected row to this one
        var indexes = dt[type + 's']({search: 'applied'}).indexes();
        var idx1 = $.inArray(last, indexes);
        var idx2 = $.inArray(idx, indexes);

        if (!dt[type + 's']({selected: true}).any() && idx1 === -1) {
            // select from top to here - slightly odd, but both Windows and Mac OS
            // do this
            indexes.splice($.inArray(idx, indexes) + 1, indexes.length);
        } else {
            // reverse so we can shift click 'up' as well as down
            if (idx1 > idx2) {
                var tmp = idx2;
                idx2 = idx1;
                idx1 = tmp;
            }

            indexes.splice(idx2 + 1, indexes.length);
            indexes.splice(0, idx1);
        }

        if (!dt[type](idx, {selected: true}).any()) {
            // Select range
            dt[type + 's'](indexes).select();
        } else {
            // Deselect range - need to keep the clicked on row selected
            indexes.splice($.inArray(idx, indexes), 1);
            dt[type + 's'](indexes).deselect();
        }
    }

    /**
     * Clear all selected items
     *
     * @param  {DataTable.settings} ctx Settings object of the host DataTable
     * @param  {boolean} [force=false] Force the de-selection to happen, regardless
     *     of selection style
     * @private
     */
    function clear(ctx, force) {
        if (force || ctx._select.style === 'single') {
            var api = new DataTable.Api(ctx);

            api.rows({selected: true}).deselect();
            api.columns({selected: true}).deselect();
            api.cells({selected: true}).deselect();
        }
    }

    /**
     * Select items based on the current configuration for style and items.
     *
     * @param  {object}             e    Mouse event object
     * @param  {DataTables.Api}     dt   DataTable
     * @param  {DataTable.settings} ctx  Settings object of the host DataTable
     * @param  {string}             type Items to select
     * @param  {int|object}         idx  Index of the item to select
     * @private
     */
    function typeSelect(e, dt, ctx, type, idx) {
        var style = dt.select.style();
        var toggleable = dt.select.toggleable();
        var isSelected = dt[type](idx, {selected: true}).any();

        if (isSelected && !toggleable) {
            return;
        }

        if (style === 'os') {
            if (e.ctrlKey || e.metaKey) {
                // Add or remove from the selection
                dt[type](idx).select(!isSelected);
            } else if (e.shiftKey) {
                if (type === 'cell') {
                    cellRange(dt, idx, ctx._select_lastCell || null);
                } else {
                    rowColumnRange(dt, type, idx, ctx._select_lastCell ?
                        ctx._select_lastCell[type] :
                        null
                    );
                }
            } else {
                // No cmd or shift click - deselect if selected, or select
                // this row only
                var selected = dt[type + 's']({selected: true});

                if (isSelected && selected.flatten().length === 1) {
                    dt[type](idx).deselect();
                } else {
                    selected.deselect();
                    dt[type](idx).select();
                }
            }
        } else if (style == 'multi+shift') {
            if (e.shiftKey) {
                if (type === 'cell') {
                    cellRange(dt, idx, ctx._select_lastCell || null);
                } else {
                    rowColumnRange(dt, type, idx, ctx._select_lastCell ?
                        ctx._select_lastCell[type] :
                        null
                    );
                }
            } else {
                dt[type](idx).select(!isSelected);
            }
        } else {
            dt[type](idx).select(!isSelected);
        }
    }

    function _safeId(node) {
        return node.id.replace(/[^a-zA-Z0-9\-\_]/g, '-');
    }


    /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
     * DataTables selectors
     */

// row and column are basically identical just assigned to different properties
// and checking a different array, so we can dynamically create the functions to
// reduce the code size
    $.each([
        {type: 'row', prop: 'aoData'},
        {type: 'column', prop: 'aoColumns'}
    ], function (i, o) {
        DataTable.ext.selector[o.type].push(function (settings, opts, indexes) {
            var selected = opts.selected;
            var data;
            var out = [];

            if (selected !== true && selected !== false) {
                return indexes;
            }

            for (var i = 0, ien = indexes.length; i < ien; i++) {
                data = settings[o.prop][indexes[i]];

                if ((selected === true && data._select_selected === true) ||
                    (selected === false && !data._select_selected)
                ) {
                    out.push(indexes[i]);
                }
            }

            return out;
        });
    });

    DataTable.ext.selector.cell.push(function (settings, opts, cells) {
        var selected = opts.selected;
        var rowData;
        var out = [];

        if (selected === undefined) {
            return cells;
        }

        for (var i = 0, ien = cells.length; i < ien; i++) {
            rowData = settings.aoData[cells[i].row];

            if ((selected === true && rowData._selected_cells && rowData._selected_cells[cells[i].column] === true) ||
                (selected === false && (!rowData._selected_cells || !rowData._selected_cells[cells[i].column]))
            ) {
                out.push(cells[i]);
            }
        }

        return out;
    });


    /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
     * DataTables API
     *
     * For complete documentation, please refer to the docs/api directory or the
     * DataTables site
     */

// Local variables to improve compression
    var apiRegister = DataTable.Api.register;
    var apiRegisterPlural = DataTable.Api.registerPlural;

    apiRegister('select()', function () {
        return this.iterator('table', function (ctx) {
            DataTable.select.init(new DataTable.Api(ctx));
        });
    });

    apiRegister('select.blurable()', function (flag) {
        if (flag === undefined) {
            return this.context[0]._select.blurable;
        }

        return this.iterator('table', function (ctx) {
            ctx._select.blurable = flag;
        });
    });

    apiRegister('select.toggleable()', function (flag) {
        if (flag === undefined) {
            return this.context[0]._select.toggleable;
        }

        return this.iterator('table', function (ctx) {
            ctx._select.toggleable = flag;
        });
    });

    apiRegister('select.info()', function (flag) {
        if (info === undefined) {
            return this.context[0]._select.info;
        }

        return this.iterator('table', function (ctx) {
            ctx._select.info = flag;
        });
    });

    apiRegister('select.items()', function (items) {
        if (items === undefined) {
            return this.context[0]._select.items;
        }

        return this.iterator('table', function (ctx) {
            ctx._select.items = items;

            eventTrigger(new DataTable.Api(ctx), 'selectItems', [items]);
        });
    });

// Takes effect from the _next_ selection. None disables future selection, but
// does not clear the current selection. Use the `deselect` methods for that
    apiRegister('select.style()', function (style) {
        if (style === undefined) {
            return this.context[0]._select.style;
        }

        return this.iterator('table', function (ctx) {
            ctx._select.style = style;

            if (!ctx._select_init) {
                init(ctx);
            }

            // Add / remove mouse event handlers. They aren't required when only
            // API selection is available
            var dt = new DataTable.Api(ctx);
            disableMouseSelection(dt);

            if (style !== 'api') {
                enableMouseSelection(dt);
            }

            eventTrigger(new DataTable.Api(ctx), 'selectStyle', [style]);
        });
    });

    apiRegister('select.selector()', function (selector) {
        if (selector === undefined) {
            return this.context[0]._select.selector;
        }

        return this.iterator('table', function (ctx) {
            disableMouseSelection(new DataTable.Api(ctx));

            ctx._select.selector = selector;

            if (ctx._select.style !== 'api') {
                enableMouseSelection(new DataTable.Api(ctx));
            }
        });
    });


    apiRegisterPlural('rows().select()', 'row().select()', function (select) {
        var api = this;

        if (select === false) {
            return this.deselect();
        }

        this.iterator('row', function (ctx, idx) {
            clear(ctx);

            ctx.aoData[idx]._select_selected = true;
            $(ctx.aoData[idx].nTr).addClass(ctx._select.className);
        });

        this.iterator('table', function (ctx, i) {
            eventTrigger(api, 'select', ['row', api[i]], true);
        });

        return this;
    });

    apiRegisterPlural('columns().select()', 'column().select()', function (select) {
        var api = this;

        if (select === false) {
            return this.deselect();
        }

        this.iterator('column', function (ctx, idx) {
            clear(ctx);

            ctx.aoColumns[idx]._select_selected = true;

            var column = new DataTable.Api(ctx).column(idx);

            $(column.header()).addClass(ctx._select.className);
            $(column.footer()).addClass(ctx._select.className);

            column.nodes().to$().addClass(ctx._select.className);
        });

        this.iterator('table', function (ctx, i) {
            eventTrigger(api, 'select', ['column', api[i]], true);
        });

        return this;
    });

    apiRegisterPlural('cells().select()', 'cell().select()', function (select) {
        var api = this;

        if (select === false) {
            return this.deselect();
        }

        this.iterator('cell', function (ctx, rowIdx, colIdx) {
            clear(ctx);

            var data = ctx.aoData[rowIdx];

            if (data._selected_cells === undefined) {
                data._selected_cells = [];
            }

            data._selected_cells[colIdx] = true;

            if (data.anCells) {
                $(data.anCells[colIdx]).addClass(ctx._select.className);
            }
        });

        this.iterator('table', function (ctx, i) {
            eventTrigger(api, 'select', ['cell', api[i]], true);
        });

        return this;
    });


    apiRegisterPlural('rows().deselect()', 'row().deselect()', function () {
        var api = this;

        this.iterator('row', function (ctx, idx) {
            ctx.aoData[idx]._select_selected = false;
            $(ctx.aoData[idx].nTr).removeClass(ctx._select.className);
        });

        this.iterator('table', function (ctx, i) {
            eventTrigger(api, 'deselect', ['row', api[i]], true);
        });

        return this;
    });

    apiRegisterPlural('columns().deselect()', 'column().deselect()', function () {
        var api = this;

        this.iterator('column', function (ctx, idx) {
            ctx.aoColumns[idx]._select_selected = false;

            var api = new DataTable.Api(ctx);
            var column = api.column(idx);

            $(column.header()).removeClass(ctx._select.className);
            $(column.footer()).removeClass(ctx._select.className);

            // Need to loop over each cell, rather than just using
            // `column().nodes()` as cells which are individually selected should
            // not have the `selected` class removed from them
            api.cells(null, idx).indexes().each(function (cellIdx) {
                var data = ctx.aoData[cellIdx.row];
                var cellSelected = data._selected_cells;

                if (data.anCells && (!cellSelected || !cellSelected[cellIdx.column])) {
                    $(data.anCells[cellIdx.column]).removeClass(ctx._select.className);
                }
            });
        });

        this.iterator('table', function (ctx, i) {
            eventTrigger(api, 'deselect', ['column', api[i]], true);
        });

        return this;
    });

    apiRegisterPlural('cells().deselect()', 'cell().deselect()', function () {
        var api = this;

        this.iterator('cell', function (ctx, rowIdx, colIdx) {
            var data = ctx.aoData[rowIdx];

            data._selected_cells[colIdx] = false;

            // Remove class only if the cells exist, and the cell is not column
            // selected, in which case the class should remain (since it is selected
            // in the column)
            if (data.anCells && !ctx.aoColumns[colIdx]._select_selected) {
                $(data.anCells[colIdx]).removeClass(ctx._select.className);
            }
        });

        this.iterator('table', function (ctx, i) {
            eventTrigger(api, 'deselect', ['cell', api[i]], true);
        });

        return this;
    });


    /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
     * Buttons
     */
    function i18n(label, def) {
        return function (dt) {
            return dt.i18n('buttons.' + label, def);
        };
    }

// Common events with suitable namespaces
    function namespacedEvents(config) {
        var unique = config._eventNamespace;

        return 'draw.dt.DT' + unique + ' select.dt.DT' + unique + ' deselect.dt.DT' + unique;
    }

    function enabled(dt, config) {
        if ($.inArray('rows', config.limitTo) !== -1 && dt.rows({selected: true}).any()) {
            return true;
        }

        if ($.inArray('columns', config.limitTo) !== -1 && dt.columns({selected: true}).any()) {
            return true;
        }

        if ($.inArray('cells', config.limitTo) !== -1 && dt.cells({selected: true}).any()) {
            return true;
        }

        return false;
    }

    var _buttonNamespace = 0;

    $.extend(DataTable.ext.buttons, {
        selected: {
            text: i18n('selected', 'Selected'),
            className: 'buttons-selected',
            limitTo: ['rows', 'columns', 'cells'],
            init: function (dt, node, config) {
                var that = this;
                config._eventNamespace = '.select' + (_buttonNamespace++);

                // .DT namespace listeners are removed by DataTables automatically
                // on table destroy
                dt.on(namespacedEvents(config), function () {
                    that.enable(enabled(dt, config));
                });

                this.disable();
            },
            destroy: function (dt, node, config) {
                dt.off(config._eventNamespace);
            }
        },
        selectedSingle: {
            text: i18n('selectedSingle', 'Selected single'),
            className: 'buttons-selected-single',
            init: function (dt, node, config) {
                var that = this;
                config._eventNamespace = '.select' + (_buttonNamespace++);

                dt.on(namespacedEvents(config), function () {
                    var count = dt.rows({selected: true}).flatten().length +
                        dt.columns({selected: true}).flatten().length +
                        dt.cells({selected: true}).flatten().length;

                    that.enable(count === 1);
                });

                this.disable();
            },
            destroy: function (dt, node, config) {
                dt.off(config._eventNamespace);
            }
        },
        selectAll: {
            text: i18n('selectAll', 'Select all'),
            className: 'buttons-select-all',
            action: function () {
                var items = this.select.items();
                this[items + 's']().select();
            }
        },
        selectNone: {
            text: i18n('selectNone', 'Deselect all'),
            className: 'buttons-select-none',
            action: function () {
                clear(this.settings()[0], true);
            },
            init: function (dt, node, config) {
                var that = this;
                config._eventNamespace = '.select' + (_buttonNamespace++);

                dt.on(namespacedEvents(config), function () {
                    var count = dt.rows({selected: true}).flatten().length +
                        dt.columns({selected: true}).flatten().length +
                        dt.cells({selected: true}).flatten().length;

                    that.enable(count > 0);
                });

                this.disable();
            },
            destroy: function (dt, node, config) {
                dt.off(config._eventNamespace);
            }
        }
    });

    $.each(['Row', 'Column', 'Cell'], function (i, item) {
        var lc = item.toLowerCase();

        DataTable.ext.buttons['select' + item + 's'] = {
            text: i18n('select' + item + 's', 'Select ' + lc + 's'),
            className: 'buttons-select-' + lc + 's',
            action: function () {
                this.select.items(lc);
            },
            init: function (dt) {
                var that = this;

                dt.on('selectItems.dt.DT', function (e, ctx, items) {
                    that.active(items === lc);
                });
            }
        };
    });


    /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
     * Initialisation
     */

// DataTables creation - check if select has been defined in the options. Note
// this required that the table be in the document! If it isn't then something
// needs to trigger this method unfortunately. The next major release of
// DataTables will rework the events and address this.
    $(document).on('preInit.dt.dtSelect', function (e, ctx) {
        if (e.namespace !== 'dt') {
            return;
        }

        DataTable.select.init(new DataTable.Api(ctx));
    });


    return DataTable.select;
}));
